<?php
namespace core\models;
/**
 * 表单验证类。注意是UTF-8格式<br />
 * 作用：实现前台js和后台php代码在验证表单数据时只需要写一次验证规则即可<br />
 * 注意：此类需要用到jQuery的表单验证插件validate<br />
 * 本类的用法可参考：Admin_My_Action::VerifyFormChangePassword
 * @see
 * https://jqueryvalidation.org/
 * https://jqueryvalidation.org/files/demo/
 * http://www.runoob.com/jquery/jquery-plugin-validate.html
 * http://blog.51yip.com/jsjquery/1483.html 如何设置二选一的例子
 * @package    扩展库
 * @author     ZhouHaoHua <53027327@qq.com>
 */
class Widget_VerifyForm {
    /**
     * 表单ID
     * @var string
     */
    private $form_id;
    /**
     * 表单提交的url地址
     * @var string
     */
    private $url;
    private $ajax;
    private $verify = array();
    private $js = array();
    private $setFielded = false;
    private $charset = 'utf-8';
    private $beforeJs = '';
    private $afterJs = '';
    private $tmp_regex = array();
    /**
     * 验证通过后的js提交表单的代码
     * @var string
     */
    private $submitHandler = '';
    /**
     * 设置错误显示的位置
     * @var string
     */
    private $errorPlacement = '';

    /**
     * 表单验证类。注意是UTF-8格式<br />
     * @param string $form_id 表单id名称。如：#from_a 代表$("#from_a")
     * @param string $url     表单提交的url地址。注意：如果在调用setSubmitHandler方法时其参数$custom_code不为空时，$url参数则无效。
     * @param bool   $ajax    是否是ajax异步提交。注意：如果在调用setSubmitHandler方法时其参数$custom_code不为空时，$ajax参数则无效。
     * @param string $charset 表单文字编码。默认是utf-8
     */
    public function __construct($form_id, $url, $ajax = true, $charset = 'utf-8') {
        $this->form_id = $form_id;
        $this->url     = $url;
        $this->ajax    = $ajax;
        $this->charset = $charset;
    }

    /**
     * 设置要验证的字段信息。<br>
     * 注意：如果$rules内容需要js代码，并且这些代码包含了"号时，请使用&quot;来替代"号，如：$("#abc")要写成$(&quot;#abc&quot;)
     * @param string $name              字段名称
     * @param array  $rules             自定义的验证规则。支持字符串和数组。中的key名请参考前端的js验证代码的文档。<br />
     *                                  数组格式如：[required: true, minlength: 5, maxlength:15] 代表必须填写，最少要输入5个字符，最大不能超过15个字符<br/>
     *                                  可选定的key大概有以下几种：<br/>
     *                                  (1)required:true|false 必输字段【bool值】<br/>
     *                                  (2)remote:"check.php" 使用ajax方法调用check.php验证输入值。是远程验证：比如注册验证用户名是否已被注册，返回值只能是true（验证成功）或false（验证失败）。<br/>
     *                                  (3)email:true 必须输入正确格式的电子邮件<br/>
     *                                  (4)url:true 必须输入正确格式的网址<br/>
     *                                  (5)date:true 必须输入正确格式的日期<br/>
     *                                  (6)dateISO:true 必须输入正确格式的日期(ISO)，例如：2009-06-23，1998/01/22 只验证格式，不验证有效性<br/>
     *                                  (7)number:true 必须输入合法的数字(负数，小数)<br/>
     *                                  (8)digits:true 必须输入整数<br/>
     *                                  (9)creditcard: 必须输入合法的信用卡号<br/>
     *                                  (10)equalTo:"#field" 输入值必须和$("#field")相同<br/>
     *                                  (11)accept: 输入拥有合法后缀名的字符串（上传文件的后缀），多个后缀之间用’|’隔开。如："gif|png|jpg"<br/>
     *                                  (12)maxlength:5 输入长度最多是5的字符串(汉字算一个字符)<br/>
     *                                  (13)minlength:10 输入长度最小是10的字符串(汉字算一个字符)<br/>
     *                                  (14)rangelength:[5,10] 输入长度必须介于 5 和 10 之间的字符串")(汉字算一个字符)；如果是数组字段则是代表至少选择5到10项<br/>
     *                                  (15)range:[5,10] 输入的数字值必须介于 5 和 10 之间<br/>
     *                                  (16)max:5 输入值不能大于5<br/>
     *                                  (17)min:10 输入值不能小于10<br/>
     *                                  (18)<b>“regex_字段名”:正则表达式匹配。</b>如：['/^d+$/','/[0-9]+/']。第一个是用于php检测，第二个是用于js检测。如果只有一个值则php和js均用相同的规则检测，这时规则就要用php的preg_math和js都兼容的格式。【注意：规则名和报错提示名必须是"regex_当前字段名",如：regex_zone，否则js会报错】
     * @param array  $messages          报错时的信息提示。数组中的key名请参考前端的js验证代码的文档。如：['required' => "必填项", 'equalTo' => "必须与第N个输入框的内容一致"]，<br/>
     */
    public function setField($name, $rules = array('required' => true), $messages = array('required' => '必填项')) {
        $this->setFielded         = true;
        $this->verify[$name]      = $this->js[$name] = array();
        $this->js[$name]['rules'] = $rules;
        if ($rules['regex_' . $name]) {
            $this->tmp_regex[$name]                     = (array) $rules['regex_' . $name];
            $this->js[$name]['rules']['regex_' . $name] = ($this->tmp_regex[$name][1] ? $this->tmp_regex[$name][1] : $this->tmp_regex[$name][0]);
            //$m                                 = $this->setAddMethod('regex', "function(value, element, params){ var exp = new RegExp(params); return exp.test(value); }", $messages['regex']);
            $m = $this->setAddMethod('regex_' . $name, "function(value, element, params){ var exp = " . $this->js[$name]['rules']['regex_' . $name] . "; return exp.test(value); }", $messages['regex_' . $name]);
            //echo $m;exit;
        }
        $this->js[$name]['messages'] = $messages;
    }

    /**
     * 设置在PHP验证时的N to n的值，在phpVerify方法中会拿这个数据去对比用户输入的数据是否符合N选n的要求
     * @var array
     */
    private $fieldN2n = array();
    /**
     * 设置N个字段中至少要填写n项<br />
     * 例如：手机 和 座机电话至少要填写1项
     * @param array $names 检测的字段名称数组，如：[phone, mobile]
     * @param int   $n 要设置多少项？如：1
     * @param string $error_messages 报错时的信息提示。如：手机和座机至少要填写1项
     * @author ZhouHaoHua <53027327@qq.com>
     */
    public function setFieldN2n(array $names, $n, $error_messages){
        $__key = md5(json_encode($names));
        foreach($names as $name){
            $rt = 'var rt = 0; ';
            foreach($names as $ns){
                if($name!=$ns){
                    $rt .= ' rt += ($(&quot;'.$this->form_id.' input[name=\''.$ns.'\']&quot;).val().length) ? 1 : 0; ';
                }
            }
            $rules = array('required' => array('depends' => 'function(){ '.$rt.' if(rt < '.(int)$n.' ){ return true; }else{ return false;} }'));
            $this->setField($name, $rules, array('required' => $error_messages));
            //设置在PHP验证时的N选n的值
            $this->fieldN2n[$__key][$name]= array('min' => $n, 'msg' => $error_messages);
        }
    }
    private $validateOthers = array();

    /**
     * 设置$("form").validate(中除了rules和messages以外的其它属性值
     * 参考：$("form").validate()中，除了rules和messages以外的值。【其值目前支持字符串和bool值，从2016-06-29以后也支持function(注意：function前不能有空格，其收尾的}号后面也不要有空格，否则报错。)】
     * @param array $array
     * @author ZhouHaoHua <53027327@qq.com>
     */
    public function setValidateOthers(array $array) {
        $this->validateOthers = $array;
    }

    /**
     * 设置JS的自定义验证方法<br>
     * 以下参数的例子是添加正则表达式验证的例子
     * @param string $name    自定义规则rules的key。如：regex
     * @param string $method  自定义验证方法的内容。如：function(value, element, params){ var exp = new RegExp(params); return exp.test(value); }
     * @param string $message 自定义的报错输出的提示。如：您输入的内容不符合本正则表达式的规则。
     * @author ZhouHaoHua <53027327@qq.com>
     * @return string
     */
    public function setAddMethod($name, $method, $message) {
        $js = '; jQuery.validator.addMethod("' . $name . '",' . $method . ',"' . $message . '");';
        $this->beforeJs .= $js;
        return $js;
    }

    /**
     * 验证提交过来的数据是否符合规则
     * @param array $data 表单提交过来的数据
     * @return array|bool 验证通过则返回true，否则返回出错的数组
     */
    public function phpVerify(array $data) {
        if ($this->setFielded !== true) {
            die('在使用此方法之前必须已经执行了setField方法');
        }
        $ok    = true;
        $error = array();
        foreach ($this->verify as $name => $val) {
            $dName = str_replace('[]', '', $name);
            if (is_array($data[$dName])) {
                $len = sizeof($data[$dName]);
            } else {
                $len = mb_strlen($data[$dName], $this->charset);
            }

            $empty = (!isset($data[$dName]) || (isset($data[$dName]) && $data[$dName] !== '0' && empty($data[$dName]))) ? true : false;
            if($this->fieldN2n) {   //多个字段联合判断时
                foreach($this->fieldN2n as $__key => $__val) {
                    $_num = 0;
                    foreach ($__val as $fn => $fv) {
                        if ($data[$fn]) {
                            $_num++;
                        }
                    }
                    if ($_num < $__val[$fn]['min']) {
                        $error[] = $__val[$fn]['msg'];
                        return $error;
                    }
                }
            }
            //==============以上代码和以下代码之间切勿使用elseif来处理============//
            if ($empty === true && $this->js[$name]['rules']['required'] == true) { //值为空或不存在时
                $error[] = !empty($this->js[$name]['messages']['required']) ? $this->js[$name]['messages']['required'] : "字段{$name}是必须项！";
            } elseif (!empty($data[$dName])) {
                //邮箱
                if ($this->js[$name]['rules']['email'] && !filter_var($data[$name], FILTER_VALIDATE_EMAIL)) {
                    $error[] = !empty($this->js[$name]['messages']['email']) ? $this->js[$name]['messages']['email'] : "字段{$name}必须是邮箱！";
                }
                //网址
                if ($this->js[$name]['rules']['url'] && !filter_var($data[$name], FILTER_VALIDATE_URL)) {
                    $error[] = !empty($this->js[$name]['messages']['url']) ? $this->js[$name]['messages']['url'] : "字段{$name}必须是合法的网址！";
                }
                //日期
                if ($this->js[$name]['rules']['date'] && (date('Y-m-d H:i:s', strtotime($data[$name])) == '1970-01-01 08:00:00' && strpos($data[$name], '1970-01-01') === false)) {
                    $error[] = !empty($this->js[$name]['messages']['date']) ? $this->js[$name]['messages']['date'] : "字段{$name}必须是合法的日期，如：2016-06-25！";
                }
                //符合格式要求的日期
                if ($this->js[$name]['rules']['dateISO'] &&
                    (date('Y-m-d', strtotime($data[$name])) != $data[$name] && date('m/d/Y', strtotime($data[$name])) != $data[$name]) &&
                    (date('Y-n-j', strtotime($data[$name])) != $data[$name] && date('n/j/Y', strtotime($data[$name])) != $data[$name])
                ) {
                    $error[] = !empty($this->js[$name]['messages']['date']) ? $this->js[$name]['messages']['date'] : "字段{$name}必须是合法的日期ISO，如：2016-06-25！";
                }
                //数字，如：1 1.12 -89
                if ($this->js[$name]['rules']['number'] && !is_numeric($data[$name])) {
                    $error[] = !empty($this->js[$name]['messages']['number']) ? $this->js[$name]['messages']['number'] : "字段{$name}必须是合法的数字，如：9, 1.02, -56！";
                }
                //整数
                if ($this->js[$name]['rules']['digits'] && !filter_var($data[$name], FILTER_VALIDATE_INT)) {
                    $error[] = !empty($this->js[$name]['messages']['digits']) ? $this->js[$name]['messages']['digits'] : "字段{$name}必须是合法的整数，如：123！";
                }
                //信用卡
                if ($this->js[$name]['rules']['creditcard'] && \Z::Widget()->CreditCard()->isCreditCard($data[$name]) !== true) {
                    $error[] = !empty($this->js[$name]['messages']['creditcard']) ? $this->js[$name]['messages']['creditcard'] : "字段{$name}必须是合法的信用卡！";
                }
                //equalTo 两字段对比要一致
                if ($this->js[$name]['rules']['equalTo']) {
                    $eq = substr($this->js[$name]['rules']['equalTo'], 1);
                    if ($data[$name] != $data[$eq]) {
                        $error[] = !empty($this->js[$name]['messages']['equalTo']) ? $this->js[$name]['messages']['equalTo'] : "字段{$name}必须与字段{$eq}的值一致！";
                    }
                }
                //accept 输入拥有合法后缀名的字符串（如上传文件的后缀）
                if ($this->js[$name]['rules']['accept'] && !preg_match('/' . preg_quote($this->js[$name]['rules']['accept']) . '$/i', $data[$name])) {
                    $error[] = !empty($this->js[$name]['messages']['accept']) ? $this->js[$name]['messages']['accept'] : "字段{$name}值必须是以{$this->js[$name]['rules']['accept']}结尾！";
                }
                //maxlength, 最多几个字 或 最多选几项
                if ($this->js[$name]['rules']['maxlength'] && $len > (int) $this->js[$name]['rules']['maxlength']) {
                    $error[] = !empty($this->js[$name]['messages']['maxlength']) ? $this->js[$name]['messages']['maxlength'] : "字段{$name}值最多可有{$this->js[$name]['rules']['maxlength']}个！";
                }
                //minlength 最少几个字 或 最少选几项
                if ($this->js[$name]['rules']['minlength'] && $len < (int) $this->js[$name]['rules']['minlength']) {
                    $error[] = !empty($this->js[$name]['messages']['minlength']) ? $this->js[$name]['messages']['minlength'] : "字段{$name}值至少要有{$this->js[$name]['rules']['minlength']}个！";
                    //print_r($error);
                }
                // rangelength
                if ($ta = $this->js[$name]['rules']['rangelength']) {
                    if ($len < $ta[0] || $len > $ta[1]) {
                        $error[] = !empty($this->js[$name]['messages']['rangelength']) ? $this->js[$name]['messages']['rangelength'] : "字段{$name}值必须有 {$ta[0]} " . ($ta[1] ? '到' : '') . " {$ta[1]} 个，您现在已有{$len}个！";
                    }
                }
                //range 介于 {min} 和 {max} 之间的值
                if ($ta = $this->js[$name]['rules']['range']) {
                    if ($data[$name] < $ta[0] || $data[$name] > $ta[1]) {
                        $error[] = !empty($this->js[$name]['messages']['range']) ? $this->js[$name]['messages']['range'] : "字段{$name}请输入介于 {$ta[0]} 和 {$ta[1]} 之间的数字！";
                    }
                }
                //max 最大值
                if ($this->js[$name]['rules']['max'] && $data[$name] > $this->js[$name]['rules']['max']) {
                    $error[] = !empty($this->js[$name]['messages']['max']) ? $this->js[$name]['messages']['max'] : "字段{$name}值大不可大于{$this->js[$name]['rules']['max']}！";
                }
                //min 最小值
                if ($this->js[$name]['rules']['min'] && $data[$name] < $this->js[$name]['rules']['min']) {
                    $error[] = !empty($this->js[$name]['messages']['min']) ? $this->js[$name]['messages']['min'] : "字段{$name}值必须不小于{$this->js[$name]['rules']['min']}！";
                }
                //remote 远程验证，不用的。
                /*if($this->js[$name]['rules']['remote'] && $data[$name]){
                    $error[] = !empty($this->js[$name]['messages']['remote']) ? $this->js[$name]['messages']['remote'] : "字段{$name}值已经存在！";
                }*/
                //regex 正则表达式验证，自定义规则的神器V^V
                if ($this->tmp_regex[$name]) {
                    if (!preg_match($this->tmp_regex[$name][0], $data[$name])) {
                        $error[] = !empty($this->js[$name]['messages']['regex_' . $name]) ? $this->js[$name]['messages']['regex_' . $name] : "字段{$name}值必须要符合正则表达式{$this->js[$name]['rules']['regex_'.$name]}的规则！";
                    }
                }
            }
        }

        if (!empty($error)) {
            //print_r($error);
            return $error;
        }
        return $ok;
    }
    /**
     * 取得js代码，以便输出到前端页面。<br />
     * 注意：<br />
     * (1)此代码不含<script>和</script>标签。<br />
     * (2)在使用此方法之前必须已经执行了setField方法<br />
     * @param bool $isDebug 是否为js测试状态，如果是就不验证后不提交数据
     * @return string
     */
    public function getJS($isDebug = false) {
        if ($this->setFielded !== true) {
            die('在使用此方法之前必须已经执行了setField方法');
        }
        $j = array();

        if ($this->validateOthers) {
            foreach ($this->validateOthers as $key => $v) {
                $j[$key] = $v;
            }
        }
        if ($isDebug !== false) {
            $j['debug'] = true;
        }
        foreach ($this->js as $name => $val) {
            $j['rules'][$name]    = $val['rules'];
            $j['messages'][$name] = $val['messages'];
        }
        if ($this->submitHandler) {
            $j['submitHandler'] = 'XXX_submitHandlerCode_XXX';
        }
        if ($this->errorPlacement) {
            $j['errorPlacement'] = 'XXX_errorPlacementCode_XXX';
        }
        $j = $this->json_encode_($j);
        //$j = stripslashes($j);
        $j = str_replace(array('"function(', '}"','"{','&quot;'),array('function(', '}','{','"'),$j);
        //$j = str_replace(array('&quot;'),array('"'),$j);
        $j = str_replace('"XXX_submitHandlerCode_XXX"', $this->submitHandler, $j);
        $j = str_replace('"XXX_errorPlacementCode_XXX"', $this->errorPlacement, $j);

        return $this->beforeJs . '; $("' . $this->form_id . '").validate(' . $j . ');' . $this->afterJs;
    }

    /**
     * 设置表单提交的代码
     * @param string $custom_code 自定义的submitHandler提交代码。如果您不满意提交方法的代码可以通过此参数自定义，如果满意就不设置此参数即可。<br />
     *                            提示：自定义的js代码必须是以function开头，如：function(form){...}，具体请参考js验证代码的中的submitHandler方法
     * @author ZhouHaoHua <53027327@qq.com>
     */
    public function setSubmitHandler($custom_code = '') {
        if (!empty($custom_code)) {
            $this->submitHandler = $custom_code;
            return;
        }
        /* 自定义 $custom_code 的例子
        $custom_code = "function(form){
            var url = '/Admin_My/';
            $.post(url, $("#passwdForm").serialize(), function(json){
                alert(json['text']);
                if(json['status']=='1'){
                    location.reload(true);
                }
            }, 'json');
            return false;
        }"*/
        $this->submitHandler = 'function(form){';
        if ($this->ajax === true) {    //ajax提交
            $this->submitHandler .= '
            $(form).find(":submit").attr({"disabled": "disabled"}).addClass("sk-circle12 sk-circle");
            var url = "' . $this->url . '";
            $.post(url, $("' . $this->form_id . '").serialize(), function(json){
                alert(json.text);
                if(json.status=="1"){
                    location.reload(true);
                }
                $(form).find(":submit").removeAttr("disabled").removeClass("sk-circle12 sk-circle");
            }, "json");
        ';
        } else {  //传统提交
            $this->submitHandler .= '
            $(form).attr({"action" : "' . $this->url . '"});
            form.submit();
            ';
        }
        $this->submitHandler .= '; return false; }';
    }

    /**
     * 取得表单提交的url地址
     * @return string
     * @author ZhouHaoHua <53027327@qq.com>
     */
    public function getUrl() {
        return $this->url;
    }

    /**
     * 取得表单ID
     * @return string
     * @author ZhouHaoHua <53027327@qq.com>
     */
    public function getFormId() {
        return $this->form_id;
    }

    /**
     * 表单验证的默认值设置。
     * 参考：$.validator.setDefaults()
     * @param array $array 具体参数。[debug => true, errorClass => 设置默认错误提示的css类名 , errorElement => 设置默认错误提示的标签名 ]，如：[errorClass => 'label label-warning' , errorElement => 'span' ]
     * @return string
     * @author ZhouHaoHua <53027327@qq.com>
     */
    public function setDefaults(array $array) {
        $js = '$.validator.setDefaults( %s );';
        $js = sprintf($js, $this->json_encode_($array));
        $this->beforeJs .= $js;
        return $js;
    }

    private function json_encode_(array $array) {
        if (substr(PHP_VERSION, 0, 3) > '5.4') {
            $j = json_encode($array, JSON_UNESCAPED_SLASHES | JSON_FORCE_OBJECT | JSON_UNESCAPED_UNICODE);
        } else {
            $j = str_replace("\\/", "/", json_encode($array));
        }
        $j = str_replace("\\\\", "\\", $j);
        return $j;
    }

    /**
     * 设置错误显示的位置 element验证的元素error错误提示
     * @author ZhouHaoHua <53027327@qq.com>
     * @param $function
     */
    public function setErrorPlacement($function) {
        /*$function = function(error,element) {
            element . parents('.form-group') . children(".help-block") . html(error);
        };*/
        $this->errorPlacement = $function;
    }
}